昨天我們說明了如何做了篩選,但其實只講了一部分,我們今天繼續來做跟篩選相關的吧!
昨天我們都是直接在 ViewSet 中設定要篩選的欄位,今天我們換個方式改用 FilterSet 的方式來做,用這個方法的好處是,我們可以將篩選的邏輯從 View 中獨立出來,方便重複利用以及後續客製化。
讓我們建立新檔案 server/app/todo/filters.py
並將下方內容貼入
from django_filters import rest_framework as filters
from server.app.todo import models as todo_models
class TagFilter(filters.FilterSet):
class Meta:
model = todo_models.Tag
fields = {
"id": ("gt", "gte", "lt", "lte"),
"name": ("exact", "contains", "icontains"),
}
這邊我們建立了 TagFilter 並在裡面的 Meta 設定 model 與 fields 代表這個 Filter 是用在哪個 Model 以及可篩選的欄位
接著我們打開 server/app/todo/views.py
並編輯
# ...... 以上省略 ......
class TagViewSet(viewsets.ModelViewSet):
queryset = todo_models.Tag.objects.order_by("id")
serializer_class = todo_serializers.TagSerializer
ordering_fields = ("id", "name")
search_fields = ("name",)
+ filterset_class = todo_filters.TagFilter
這邊我們是設定 TagViewSet 要使用 TagFilter 來實現篩選相關的功能
P.S. 這邊補充說明一下,其實我們昨天在 TaskViewSet 裡面設定的也會自動的轉成這個樣子。
現在大家可以試試看使用 GET 請求下方的網址(別忘了啟用虛擬環境以及 server 唷)
P.S. 一樣在 SQLite 的情況下 contains 也不會區分大小寫但其他資料庫就會了,詳細可以看昨天的說明
大家可以仔細觀察一下當我想找名稱為某個的時候(對應到 name 欄位的 exact 篩選),我們在 url 中不用特別用雙底線指定要如何篩選,是因為預設的篩選模式就是 exact 所以 Filter 在產生搜尋的參數名稱時後面不會有篩選方法。
前面我們都是直接針對 Model 的欄位進行篩選,但不是每次都這麼幸運我們的篩選都會是欄位,所以我們就會需要進行 Filter 的客製化,假設我們有兩個需求
has_task
讓使用者傳入 true 或 false 列出有被任務使用的標籤與沒有的。task_count
讓使用者傳入數字列有指定個任務的標籤(例如傳入 3 就列只列出有三個任務的標籤)讓我們試著用客製化的方式完成上方的兩個需求吧!
讓我們編輯 server/app/todo/filters.py
+from django.db import models
from django_filters import rest_framework as filters
from server.app.todo import models as todo_models
class TagFilter(filters.FilterSet):
+ has_task = filters.BooleanFilter(method="has_task_filter")
+
class Meta:
model = todo_models.Tag
fields = {
"id": ("gt", "gte", "lt", "lte"),
"name": ("exact", "contains", "icontains"),
}
+ def has_task_filter(self, queryset, name, value):
+ qs = queryset.alias(task_count=models.Count("task"))
+
+ if value:
+ return qs.filter(task_count__gt=0)
+
+ return qs.filter(task_count=0)
這邊我們在 TagFilter
中新增了一個欄位名為 has_task 並設定他是一個布林的篩選欄位,同時設定當執行這個篩選時呼叫 has_task_filter
方法
而 has_task_filter
方法接收三個參數(這個 django-filters 套件規定的)分別是 queryset 代表目前資料有哪些還有 name 代表的是篩選欄位名稱(這邊會是 has_task
)以及 value 會是使用者傳入的值(True 或 False)
接著讓我們看看在 has_task_filter
方法裡面做了哪些事情
qs = queryset.alias(task_count=models.Count("task"))
這個 ORM 語法的目的是在 queryset 中的物件加一個 task_count 欄位僅供 orm 過濾時使用,task_count 存放的是當前標籤被幾個任務使用,並將加過欄位的 queryset 放到變數 qs 中。qs.filter(task_count__gt=0)
代表回傳被大於 0 個任務使用的標籤(代表有任務使用此標籤),反之回傳 qs.filter(task_count=0)
回傳沒有被任務使用的標籤。總結來說,原本 task_count
欄位不存在於 Tag model 中,所以本來是無法使用 .filter()
的語法過濾資料,但我們透過 alias 的方式加上一個過濾可以使用的欄位讓我們可以根據使用者傳入的 value 進行篩選。
現在我們可以使用 GET 方法請求以下網址
接著讓我們繼續做需求二,我們持續編輯 server/app/todo/filters.py
from django.db import models
from django_filters import rest_framework as filters
from server.app.todo import models as todo_models
class TagFilter(filters.FilterSet):
has_task = filters.BooleanFilter(method="has_task_filter")
+ task_count = filters.NumberFilter(method="task_count_filter")
class Meta:
model = todo_models.Tag
fields = {
"id": ("gt", "gte", "lt", "lte"),
"name": ("exact", "contains", "icontains"),
}
def has_task_filter(self, queryset, name, value):
qs = queryset.alias(task_count=models.Count("task"))
if value:
return qs.filter(task_count__gt=0)
return qs.filter(task_count=0)
+ def task_count_filter(self, queryset, name, value):
+ return queryset.alias(task_count=models.Count("task")).filter(task_count=value)
這邊跟前面一樣只是我們加的欄位是 task_count 且型態為數字,並且當他被執行時會呼叫 task_count_filter 方法,裡面使用 .alias()
在 queryset 中加一個欄位存放 task 數量,並供後面的 .filter()
過濾使用,並根據使用者傳入的值找出被指定數量任務使用的標籤。
今天我們學習了如何使用 FilterSet 建立 Filter 與如何客製化 Filter。
結束前別忘了檢查一下今天的程式碼有沒有問題,並排版好喔。
ruff check --fix .
black .
pyright .
今天的內容就到這邊了,讓我們期待明天的內容吧。
P.S. 今天的檔案更新可以參考我的 Git Commit 大家可以搭配服用